﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;

using ABCat.Shared.Properties;

namespace ABCat.Core.Plugins
{
	[AttributeUsage(AttributeTargets.Class)]
	public sealed class PluginCreatorAttribute : Attribute
	{
		#region + Fields +

		/// <summary>
		/// Имя плагина
		/// </summary>
		public readonly string Name;
		public readonly WorkModeEnum WorkMode;

		private static readonly object LockObj = new object();
		private static readonly ConcurrentDictionary<Type, IPlugin> SingletoneInstances = new ConcurrentDictionary<Type, IPlugin>();

		private volatile IPlugin _perContextInstance;
		private Type _pluginType;

		#endregion
		#region + Properties +

		public Config Config		{get; set;}
		public Type ConfigType		{get; set;}
		public bool IsEnabled		{get; set;}
		public Action<IPlugin> OnInstanceCreatedAction		{get; set;}
		public Action<IPlugin> OnInstanceDisposedAction		{get; set;}
		/// <summary>
		/// Тип плагина (назначается при загрузке плагина в Runtime)
		/// </summary>
		public Type PluginType
		{
			get { return _pluginType; }
			private set
			{
				if (_pluginType == value) return;
				_pluginType = value;
			}
		}

		#endregion
		#region + Ctor +

		/// <summary>
		/// Аттрибут, описывающий тип плагина
		/// </summary>
		/// <param name="name">Имя плагина</param>
		/// <param name="workMode">Режим кеширования экземпляров плагина</param>
		/// <param name="isEnabled">Плагин включен (иначе игнорируется при загрузке плагинов)</param>
		public PluginCreatorAttribute([NotNull] string name, WorkModeEnum workMode, bool isEnabled = true)
		{
			IsEnabled = isEnabled;
			WorkMode = workMode;
			if (string.IsNullOrEmpty(name)) throw new Exception("Name cannot be Null");

			Name = name;
		}

		#endregion
		#region + Logic +

		/// <summary>
		/// Создать экземпляр плагина
		/// </summary>
		/// <typeparam name="T">Тип экземпляра плагина</typeparam>
		/// <param name="context">Контекст выполнения</param>
		/// <returns>Экземпляр плагина</returns>
		[NotNull]
		public T CreateInstance<T>([NotNull] Context context)
			where T : IPlugin
		{
			T result;
			bool createdNew = false;

			switch (WorkMode)
			{
				default:
				case WorkModeEnum.PerCall:
					result = (T)Activator.CreateInstance(PluginType, context);
					createdNew = true;
					break;
				case WorkModeEnum.PerContext:
					if (_perContextInstance == null)
					{
						lock (LockObj)
						{
							if (_perContextInstance == null)
							{
								var newPerContextInstance = (IPlugin)Activator.CreateInstance(PluginType, context);
								newPerContextInstance.Disposed += _perContextInstance_Disposed;
								_perContextInstance = newPerContextInstance;
								createdNew = true;
							}
						}
					}
					result = (T)_perContextInstance;
					break;
				case WorkModeEnum.Singletone:
					IPlugin plugin;
					if (!SingletoneInstances.TryGetValue(PluginType, out plugin))
					{
						lock (LockObj)
						{
							if (!SingletoneInstances.TryGetValue(PluginType, out plugin))
							{
								plugin = (IPlugin)Activator.CreateInstance(PluginType, context);
								SingletoneInstances.TryAdd(PluginType, plugin);
								createdNew = true;
							}
						}
					}
					result = (T)plugin;
					break;
			}

			if (createdNew)
			{
				OnInstanceCreatedAction(result);
				result.Disposed += result_Disposed;
			}

			Config incorrectConfig;
			if (!result.CheckForConfig(false, out incorrectConfig))
			{
				context.ShowConfigWindow(incorrectConfig);
			}

			return result;
		}

		/// <summary>
		/// Инициализация
		/// </summary>
		/// <param name="pluginType">Тип плагина</param>
		public void Init([NotNull] Type pluginType)
		{
			PluginType = pluginType;
		}

		private void result_Disposed(object sender, EventArgs e)
		{
			var plugin = (IPlugin)sender;
			plugin.Disposed -= result_Disposed;
			OnInstanceDisposedAction((IPlugin)sender);
		}

		private void _perContextInstance_Disposed(object sender, EventArgs e)
		{
			lock (LockObj)
			{
				if (_perContextInstance == sender) _perContextInstance = null;
			}
		}

		#endregion
	}
}